namespace TomatoBreak.DataAccess
{
    using System;
    using System.Linq;

    using NHibernate;
    using NHibernate.Cfg;
    using NHibernate.Dialect;
    using NHibernate.Driver;
    using NHibernate.Mapping.ByCode;
    using NHibernate.Tool.hbm2ddl;

    using Infrastructure;
    using Mapping;

    public class SessionProvider : Disposable
    {
        private static readonly Type thisType = typeof(SessionProvider);
        private static readonly Type classMapType = typeof(IClassMap);
        private static readonly Type stringType = typeof(string);
        private static readonly Type nullableType = typeof(Nullable<>);

        private static readonly object configurationLock = new object();
        private static readonly object sessionFactoryLock = new object();

        private readonly string connectionString;
        private readonly string providerName;

        private static Configuration configuration;
        private static ISessionFactory sessionFactory;

        private ISession session;

        public SessionProvider(string connectionString, string providerName)
        {
            if (string.IsNullOrWhiteSpace(connectionString))
            {
                throw new ArgumentException(
                    ErrorMessages.SessionProvider_Ctor_Blank_Connection_String,
                    "connectionString");
            }

            if (string.IsNullOrWhiteSpace(providerName))
            {
                throw new ArgumentException(
                    ErrorMessages.SessionProvider_Ctor_Blank_Provider,
                    "providerName");
            }

            this.connectionString = connectionString;
            this.providerName = providerName;
        }

        public virtual ISession GetSession()
        {
            if (session == null)
            {
                EnsureSessionFactory(connectionString, providerName);
                session = sessionFactory.OpenSession();
            }

            return session;
        }

        public virtual void CreateSchema()
        {
            GetSchema().Create(false, true);
        }

        public virtual void DropSchema()
        {
            GetSchema().Drop(false, true);
        }

        protected override void DisposeCore()
        {
            if (session != null)
            {
                session.Dispose();
            }
        }

        private static Configuration BuildConfiguration(
            string connectionString, string providerName)
        {
            var mapper = new ModelMapper();

            mapper.AfterMapClass += (inspector, type, customizer) =>
                customizer.Id(m => m.Generator(Generators.GuidComb));

            mapper.AfterMapProperty += (inspector, member, customizer) =>
            {
                var memberType = member.LocalMember.GetPropertyOrFieldType();

                if (memberType.IsGenericType &&
                    nullableType.IsAssignableFrom(
                    memberType.GetGenericTypeDefinition()))
                {
                    customizer.NotNullable(false);
                }
                else if (!stringType.IsAssignableFrom(memberType))
                {
                    customizer.NotNullable(true);
                }
            };

            foreach (var map in thisType.Assembly
                                        .GetTypes()
                                        .Where(t =>
                                            t.IsClass &&
                                            !t.IsAbstract &&
                                            classMapType.IsAssignableFrom(t))
                                        .Select(Activator.CreateInstance)
                                        .OfType<IClassMap>())
            {
                map.Map(mapper);
            }

            var cfg = new Configuration();

            cfg.DataBaseIntegration(c =>
            {
                c.ConnectionString = connectionString;

                if (providerName.Equals(
                    "System.Data.SqlServerCe.4.0",
                    StringComparison.OrdinalIgnoreCase))
                {
                    c.Driver<SqlServerCeDriver>();
                    c.Dialect<MsSqlCe40Dialect>();
                }
                else
                {
                    c.Driver<SqlClientDriver>();
                    c.Dialect<MsSql2008Dialect>();
                }

                #if DEBUG
                    c.LogSqlInConsole = true;
                    c.LogFormattedSql = true;
                #endif

                c.KeywordsAutoImport = Hbm2DDLKeyWords.AutoQuote;
                c.SchemaAction = SchemaAutoAction.Create;
            });

            cfg.AddMapping(
                mapper.CompileMappingForAllExplicitlyAddedEntities());

            SchemaMetadataUpdater.QuoteTableAndColumns(cfg);

            return cfg;
        }

        private static void EnsureSessionFactory(
            string connectionString, string providerName)
        {
            EnsureConfiguration(connectionString, providerName);

            if (sessionFactory != null)
            {
                return;
            }

            lock (sessionFactoryLock)
            {
                if (sessionFactory != null)
                {
                    return;
                }

                sessionFactory = configuration.BuildSessionFactory();
            }
        }

        private static void EnsureConfiguration(
            string connectionString, string providerName)
        {
            if (configuration != null)
            {
                return;
            }

            lock (configurationLock)
            {
                if (configuration != null)
                {
                    return;
                }

                configuration = BuildConfiguration(
                    connectionString, providerName);
            }
        }

        private SchemaExport GetSchema()
        {
            EnsureConfiguration(connectionString, providerName);

            return new SchemaExport(configuration);
        }
    }
}